package com.ruoyi.eqmonitor.dispose.mapper;

import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
import com.baomidou.lock.executor.RedisTemplateLockExecutor;
import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.eqmonitor.dispose.domain.ExteriorMonitorData;
import com.ruoyi.eqmonitor.dispose.domain.OpentsdbTags;
import com.ruoyi.eqmonitor.dispose.service.IExteriorMonitorDataService;
import com.ruoyi.eqmonitor.utils.IotUtils;
import com.ruoyi.eqmonitor.domain.OptDeviceMonitorLastData;
import com.ruoyi.eqmonitor.dtos.OptDeviceDto;
import com.ruoyi.eqmonitor.exception.IoTDBException;
import com.ruoyi.eqmonitor.exception.OpentsdbValidateException;
import com.ruoyi.eqmonitor.utils.Constant;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.*;

/**
 * 监测数据写入与重试
 *
 * @author dh
 */
@Slf4j
@Component
public class MonitorDataIotDBMapper {

    private static final Logger dataLogger = LoggerFactory.getLogger("monitor_data");

    /**
     * 最新实时数据Redis key
     */
    public static final String LAST_MONITOR_DATA_REDIS_KEY = "last_monitor_data:";
    /**
     * 当天回传数据的全部日期Redis key
     */
    public static final String TODAY_MONITOR_DATE_REDIS_KEY = "today_monitor_date:";
    /**
     * 存储锁模板
     */
    private static final String LOCK_TEMPLATE_KEY = "{}_opentsdb_save_lock";

    @Resource
    private RedisService redisService;
    @Resource
    private IExteriorMonitorDataService iExteriorMonitorDataService;
    @Resource
    private LockTemplate lockTemplate;
    @Resource
    private IotUtils iotUtils;
    @Lazy
    @Resource
    private MonitorDataIotDBMapper self;

    /**
     * 写入监测数据
     * 重试3次, 第一次重试为2秒，第二次为2秒，第三次为2秒 不包含 OpentsdbValidateException
     * 存储格式:
     * root.hx.tz.0C21JD.0x0501 = value
     * root.hx.tz.0C21JD.0x05D1 = value
     */
    public void write(String snCode, OptDeviceDto optDevice, JSONArray messageArray, String version) throws IoTDBException {

        String insertDeviceCode = optDevice.getDeviceCode();
        // 1. 校验消息体是否为空
        if (messageArray == null || messageArray.isEmpty()) {
            throw new OpentsdbValidateException(StrUtil.format("snCode:{}, deviceCode: {}, 消息流水: {}, 写入opentsdb失败, 消息体不存在或为空: {}", snCode, insertDeviceCode, version, messageArray));
        }
        if (StrUtil.isBlank(insertDeviceCode)) {
            throw new OpentsdbValidateException(StrUtil.format("snCode:{}, deviceCode: {}, 消息流水: {}, 写入opentsdb失败, deviceCode为空: {}", snCode, insertDeviceCode, version, messageArray));
        }

        // 2. 批量处理数据
        String lockKey = StrUtil.format(LOCK_TEMPLATE_KEY, insertDeviceCode);
        // 获取锁
//        final LockInfo lockInfo = lockTemplate.lock(lockKey, 120000L, 150000L, RedisTemplateLockExecutor.class);
        /* TODO 台州服务器存储较慢, 临时扩大下锁的超时时间 先给个20分钟 */
        final LockInfo lockInfo = lockTemplate.lock(lockKey, 1000L * 60 * 20, 1000L * 60 * 20 + 3000, RedisTemplateLockExecutor.class);
        if (null == lockInfo) {
            throw new RuntimeException(StrUtil.format("snCode:{}, deviceCode: {}, 消息流水: {}, 执行存储获取锁失败", snCode, insertDeviceCode, version));
        }
        // 3. 处理数据格式
        try {
            for (int i = 0; i < messageArray.size(); i++) {
                try {
                    JSONObject message = messageArray.getJSONObject(i);
                    Constant.MonitorInfo info = Constant.MonitorInfo.getInfoByCode(message.getStr("monitorCode"));
                    // 3.1 没有这个监测指标 或者这个监测指标不允许存储 则抛弃
                    if (ObjectUtil.isNull(info) || !info.isSave()) {
                        continue;
                    }
                    // 3.2 日期不存在 则抛弃
                    String monitorTime = message.getStr("monitorTime", null);
                    if (StrUtil.isEmpty(monitorTime)) {
                        continue;
                    }
                    DateTime parseDate = DateUtil.parse(monitorTime);

                    Double monitorData = message.getDouble("monitorData", 0D);
                    // 3.4 校验并创建tags对象 如果转换失败 则抛弃
                    OpentsdbTags opentsdbTags = validAndBuildTags(optDevice, message);
                    if (opentsdbTags == null) continue;

                    // 3.5 创建mysql需要的exteriorMonitorData对象
                    ExteriorMonitorData exteriorMonitorData = ExteriorMonitorData.buildByOpentsdbTags(parseDate, monitorData, opentsdbTags);
                    // 3.6 创建opentsdb需要的point对象 [metric, 时间戳, tag值]
                    OptDeviceMonitorLastData currentData = buildDeviceMonitorLastData(optDevice, opentsdbTags, parseDate, monitorData);
                    // 一条条存储
                    self.saveMonitorData(insertDeviceCode, version, exteriorMonitorData, currentData);
                } catch (Exception e) {
                    dataLogger.error("snCode:{}, deviceCode: {}, 消息流水: {}, 解析监测数据失败", snCode, insertDeviceCode, version, e);
                }
            }
        } catch (Exception e) {
            dataLogger.warn("IotDB 存储失败: {}", e.getMessage());
        } finally {
            //释放锁
            lockTemplate.releaseLock(lockInfo);
        }
    }

    /**
     * 存储需要的内容
     *
     * @param insertDeviceCode   设备deviceCode
     * @param version            消息流水
     * @param monitorData        监测数据列表 (存储mysql用)
     * @param point              监测数据列表 (存储ioTDB用)
     */
    @Transactional(rollbackFor = Exception.class)
    public void saveMonitorData(String insertDeviceCode,
                                String version,
                                ExteriorMonitorData monitorData,
                                OptDeviceMonitorLastData point) {
        // 1. 入库关系数据库 -- 主要是去重使用
        iExteriorMonitorDataService.insert(monitorData);
        // 2. 序列化最新数据
        saveOrUpdateMonitorLastData(insertDeviceCode, point);
        // 3 执行IOT存储
        iotUtils.write(point, version, insertDeviceCode);
        // 4. 记录当天都有哪些日期的数据回传了
        redisService.setCacheMapValue(TODAY_MONITOR_DATE_REDIS_KEY, DateUtil.format(point.getMonitorTime(), "yyyy-MM-dd"), 1);
    }

    /**
     * 更新最后的监测数据
     *
     * @param snCode   设备编码
     * @param data 最后监测数据列表
     */
    private void saveOrUpdateMonitorLastData(String snCode, OptDeviceMonitorLastData data) {
        String redisHKey = getRedisHKey(snCode);
        String monitorCode = data.getCode();
        // 原始数据
        OptDeviceMonitorLastData lastData = redisService.getCacheMapValue(redisHKey, monitorCode);
        if (lastData == null || DateUtil.compare(lastData.getMonitorTime(), data.getMonitorTime()) < 0) {
            redisService.setCacheMapValue(redisHKey, monitorCode, data);
        }
    }

    /**
     * 最后监测数据的hKey
     *
     * @param snCode 设备编码
     * @return redis hKey
     */
    public static String getRedisHKey(String snCode) {
        return LAST_MONITOR_DATA_REDIS_KEY.concat(snCode);
    }

    /**
     * 创建数据库需要的 OptDeviceMonitorLastData 对象
     *
     * @param optDevice    设备信息
     * @param opentsdbTags opentsdb数据项
     * @param monitorTime  数据时间
     * @param monitorData  数据值
     * @return OptDeviceMonitorLastData对象
     */
    private OptDeviceMonitorLastData buildDeviceMonitorLastData(OptDeviceDto optDevice, OpentsdbTags opentsdbTags, Date monitorTime, Double monitorData) {
        OptDeviceMonitorLastData deviceMonitorLastData = new OptDeviceMonitorLastData();
        deviceMonitorLastData.setDeviceName(optDevice.getDeviceName());
        deviceMonitorLastData.setDeviceCode(optDevice.getDeviceCode());
        deviceMonitorLastData.setSiteName(optDevice.getSiteName());
        deviceMonitorLastData.setDeviceType(optDevice.getDeviceType());
        deviceMonitorLastData.setInstallTime(optDevice.getInstallTime());
        deviceMonitorLastData.setAbx(optDevice.getAbx());
        deviceMonitorLastData.setAby(optDevice.getAby());
        deviceMonitorLastData.setCode(opentsdbTags.getMonitorCode());
        deviceMonitorLastData.setCodeValue(monitorData);
        deviceMonitorLastData.setDataUnit(opentsdbTags.getDataUnit());
        deviceMonitorLastData.setMonitorTime(monitorTime);
        deviceMonitorLastData.setMonitorName(opentsdbTags.getMonitorName());
        return deviceMonitorLastData;
    }


    /**
     * 校验并构建tags对象
     *
     * @param optDevice 设备对象
     * @param message   设备消息
     * @return tags对象
     */
    private OpentsdbTags validAndBuildTags(OptDeviceDto optDevice, JSONObject message) {
        // 1. 取出并校验数据
        String snCode = optDevice.getDeviceCode();
        String monitorCode = message.getStr("monitorCode");
        Constant.MonitorInfo monitorInfo;
        Constant.MonitorInfo monitorCodeEnum = Constant.MonitorInfo.getMonitorInfoByCode(monitorCode);
        Constant.MonitorInfo paramCodeEnum = Constant.MonitorInfo.getParamInfoByCode(monitorCode);
        monitorInfo = monitorCodeEnum == null ? paramCodeEnum : monitorCodeEnum;
        if (monitorInfo == null || !monitorInfo.isSave()) {
            if (monitorInfo == null) {
                // 这里不存在的监测指标, 打一个日志后放弃处理
                log.warn("监测指标不存在: {}, 数据: {}", monitorCode, message);
            }
            return null;
        }
        // 注意: 不要轻易使用设备回传的如下两个参数 可能与实际参数定义产生差别, 需要自行维护监测项名称
        // 监测指标名称
        String dataUnit = message.getStr("dataUnit", monitorInfo.getUnit());
        // 监测指标单位
        String monitorName = StrUtil.isEmpty(monitorInfo.getName()) ?
                message.getStr("monitorName", StrUtil.EMPTY) : monitorInfo.getName();


        // 监测数据值
        Double monitorData = message.getDouble("monitorData");

        if (StrUtil.isEmpty(monitorCode)) {
            throw new OpentsdbValidateException(snCode + " 解析监测数据失败, 监测指标不存在" + message);
        } else if (ObjectUtil.isNull(monitorData)) {
            throw new OpentsdbValidateException(snCode + " 解析监测数据失败, 监测数据值不存在" + message);
        }

        // 2. 将数据序列化为opentsdb需要的格式
        OpentsdbTags opentsdbTags = new OpentsdbTags();
        opentsdbTags.setSnCode(snCode);
        opentsdbTags.setMonitorCode(monitorCode);
        opentsdbTags.setMonitorName(monitorName);
        opentsdbTags.setDataUnit(dataUnit);
        String equipmentType = Constant.EquipmentType.getValueByCode(optDevice.getDeviceType());
        opentsdbTags.setEquipmentType(equipmentType);
        opentsdbTags.setEquipmentTypeCode(optDevice.getDeviceType());

        return opentsdbTags;
    }

}
